package ge.tsu.java.project.commons;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;

public class BinaryProtocol implements Protocol {

	private InputStream in;
	private OutputStream out;

	public BinaryProtocol(InputStream in, OutputStream out) {
		if (in == null) {
			throw new NullPointerException("in is null");
		}
		if (out == null) {
			throw new NullPointerException("out is null");
		}
		this.in = in;
		this.out = out;
	}

	@Override
	public int available() throws IOException {
		return in.available();
	}
	
	@Override
	public void writeMessage(Message message) throws IOException {
		writeSerializable(message);
	};
	
	@Override
	public void writeSerializable(Serializable serializable) throws IOException {
		ObjectOutputStream oos = new ObjectOutputStream(out);
		oos.writeObject(serializable);
	}
	
	@Override
	public void writeByte(byte value) throws IOException {
		write(new byte[] {value}, 0, 1);
	}
	
	@Override
	public void writeString(String value) throws IOException {
		byte[] bytes;
		try {
			bytes = value.getBytes("UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("This JVM does not support UTF-8");
		}
		writeInt(bytes.length);
		write(bytes);
	}

	void write(byte[] arr) throws IOException {
		write(arr, 0, arr.length);
	}

	@Override
	public void writeInt(int value) throws IOException {
		byte[] bytes = new byte[4];
		bytes[0] = (byte) (0xFF & (value >> 24));
		bytes[1] = (byte) (0xFF & (value >> 16));
		bytes[2] = (byte) (0xFF & (value >> 8));
		bytes[3] = (byte) (0xFF & (value >> 0));
		write(bytes);
	}

	@Override
	public void writeLong(long value) throws IOException {
		int left = (int) (0xFFFFFFFF & (value >> 32));
		int right = (int) (0xFFFFFFFF & (value >> 0));
		writeInt(left);
		writeInt(right);
	}

	@Override
	public void writeFloat(float value) throws IOException {
		writeInt(Float.floatToIntBits(value));
	}

	@Override
	public void writeDouble(double value) throws IOException {
		writeLong(Double.doubleToLongBits(value));
	}

	@Override
	public void writeBoolean(boolean value) throws IOException {
		writeInt(value ? 1 : 0);
	}
	
	@Override
	public void write(byte[] arr, int off, int len) throws IOException {
		out.write(arr, off, len);
	}

	@Override
	public Message readMessage() throws IOException {
		Serializable serializable = readSerializable();
		try {
			return (Message) serializable;
		} catch (ClassCastException e) {
			throw new RuntimeException("Requested to read Message, read instead: " + 
					serializable.getClass());
		}
	}
	
	@Override
	public Serializable readSerializable() throws IOException {
		ObjectInputStream ois = new ObjectInputStream(in);
		try {
			return (Serializable)ois.readObject();
		} catch (ClassNotFoundException e) {
			throw new RuntimeException("Unable to read Serializable", e);
		} catch (ClassCastException e) {
			return null;
		}
	}
	
	@Override
	public byte readByte() throws IOException {
		byte[] oneByte = new byte[1];
		read(oneByte, 0, 1);
		return oneByte[0];
	}
	
	@Override
	public String readString() throws IOException {
		int len = readInt();
		byte[] strBytes = new byte[len];
		read(strBytes, 0, len);
		return new String(strBytes, "UTF-8");
	}

	@Override
	public int readInt() throws IOException {
		byte[] bytes = new byte[4];
		read(bytes, 0, 4);
		int value = (int) bytes[0] << 24;
		value |= (int) bytes[1] << 16;
		value |= (int) bytes[2] << 8;
		value |= (int) bytes[3] << 0;
		return value;
	}

	@Override
	public long readLong() throws IOException {
		int left = readInt();
		int right = readInt();
		return ((long) left << 32) | right;
	}

	@Override
	public float readFloat() throws IOException {
		return Float.intBitsToFloat(readInt());
	}

	@Override
	public double readDouble() throws IOException {
		return Double.longBitsToDouble(readLong());
	}

	@Override
	public boolean readBoolean() throws IOException {
		return readInt() == 1;
	}
	
	@Override
	public int read(byte[] arr, int off, int len) throws IOException {
		return in.read(arr, off, len);
	}

}
